package angel.core.freemarker;

import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.WarningLevel;

import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;

public class Shrinksafe implements TemplateDirectiveModel {

	private final static Logger logger = LoggerFactory.getLogger(Shrinksafe.class);
	
	private final static Map<String, String> cache = java.util.Collections.synchronizedMap(new HashMap<String, String>());
	private final static Properties KEYWORDS = new Properties();
	private static String[] keyWords = null;
	
	static{
		try{
			if(KEYWORDS.isEmpty()){
				KEYWORDS.load(Shrinksafe.class.getResourceAsStream("ShrinksafeKeywords.properties"));
			}
						
			keyWords = KEYWORDS.keySet().toArray(new String[]{});
			Arrays.sort(keyWords);
			
			logger.info("keyWords: {} {}",keyWords.length,keyWords);
			
		}
		catch(IOException e){
			logger.error("Failed to load ShrinksafeKeywords.properties",e);
		}
	}
	
	public Shrinksafe() {
				
	}

	public void execute(Environment env, Map params, TemplateModel[] loopVars,
			TemplateDirectiveBody body) throws TemplateException, IOException {
		
		if (params.isEmpty()) {
			throw new IllegalArgumentException("Parameter is required");
		}
				
		String key = ((freemarker.template.SimpleScalar) params.get("key")).getAsString();
		boolean cached = ((freemarker.template.TemplateBooleanModel) params.get("cached")).getAsBoolean();
		boolean shrink = ((freemarker.template.TemplateBooleanModel) params.get("shrink")).getAsBoolean();
				
		if (cached && cache.containsKey(key)) {
			logger.info("Shrinksafe from cache, key = {}",key);
			env.getOut().write(cache.get(key));
		} else {
			if (body != null) {
				if(shrink){	
					body.render(new ShrinksafeWriter(env.getOut(),params,env));
				}else{
					body.render(env.getOut());
				}
			}			
		}			
		
	}

	private class ShrinksafeWriter extends Writer {
		private final Writer out;
		private StringBuilder body = new StringBuilder();
		private String key = null;
		private Environment env = null;
		private Map params = null;
		
		ShrinksafeWriter(Writer out, Map params,Environment env) {
			this.out = out;			
			this.env = env;
			this.params = params;
		}

		@Override
		public void close() throws IOException {
			// System.out.println("[ShrinksafeWriter]close:" + body.length());
			out.close();
		}

		@Override
		public void flush() throws IOException {
			// System.out.println("[ShrinksafeWriter]flush:" + body.length());
			out.flush();
		}

		@Override
		public void write(char[] cbuf, int off, int len) throws IOException {			
				body.append(cbuf, off, len);
				String END_OF_shrinksafe = "/*END_OF_shrinksafe*/";
				if (body.lastIndexOf(END_OF_shrinksafe) != -1) {
					try{	
						String script = body.toString();
						body.delete(0, body.length());
						//script = script.substring(0, script.length() - END_OF_shrinksafe.length() - 2);
						script = script.replace(END_OF_shrinksafe, "");
						
//						if(env.getVariable("ShrinksafeKeyWords") != null){
//							String[] keyWords = ((freemarker.template.SimpleScalar)env.getVariable("ShrinksafeKeyWords")).getAsString().split(";");							
//							Set<String> hashSet = new HashSet<String>();
//							hashSet.addAll(Arrays.asList(keyWords));
//							hashSet.remove("");
//							keyWords = hashSet.toArray(new String[]{});
//							Arrays.sort(keyWords);
////							logger.info("####Keywords {} {}",keyWords.length,keyWords);
//							for(int i = keyWords.length - 1; i >= 0; i--){	
//								String replacement = KEYWORDS.getProperty(keyWords[i]);
//								if(replacement == null){
//									int index = KEYWORDS.size();
//									do{
//										replacement = "w" + index++;
//									}
//									while(KEYWORDS.containsValue(replacement));
//										
//									KEYWORDS.setProperty(keyWords[i], replacement);
//									logger.error("**Replacement for new keyword** {}={}",keyWords[i], replacement);
//								}
//								script = script.replaceAll(keyWords[i],replacement);							
//							}							
//							logger.info("source replacement completed...");	
//						}

						boolean replaceKeyword = true;
						if(env.getVariable("replaceKeyword") != null){
							String level = ((freemarker.template.SimpleScalar)env.getVariable("replaceKeyword")).getAsString();
							replaceKeyword = "true".equalsIgnoreCase(level);
						}
						
						if(keyWords != null && replaceKeyword){
							for(int i = keyWords.length - 1; i >= 0; i--){	
								String replacement = KEYWORDS.getProperty(keyWords[i]);
								if(replacement == null){
									int index = KEYWORDS.size();
									do{
										replacement = "w" + index++;
									}
									while(KEYWORDS.containsValue(replacement));		
									
									logger.error("**Replacement for new keyword** {}={}",keyWords[i], replacement);
								}
								script = script.replaceAll(keyWords[i],replacement);							
							}							
							logger.info("source replacement completed...");	
						}
						
						
						String key = ((freemarker.template.SimpleScalar) params.get("key")).getAsString();
						boolean cached = ((freemarker.template.TemplateBooleanModel) params.get("cached")).getAsBoolean();
						if(params.get("contentType") != null){
							String contentType = ((freemarker.template.SimpleScalar) params.get("contentType")).getAsString();
							if(contentType.equals("html")){
								//remove dummy spaces from html code
								script = script.replaceAll("\\s{2,}", " ");
								
								out.write(script);
								if (cached && key != null) {
									cache.put(key, script);
								}
								
								return;
							}else if(contentType.equals("css")){
								script = script.replaceAll("\\s{2,}", "").replaceAll("}", "}\n");
								
								out.write(script);
								if (cached && key != null) {
									cache.put(key, script);
								}
								
								return;
							}
						}
						
						String compilationLevel = "SIMPLE_OPTIMIZATIONS";
						//default compilation level for all template
						if(env.getVariable("CompilationLevel") != null){
							String level = ((freemarker.template.SimpleScalar)env.getVariable("CompilationLevel")).getAsString();
							if(StringUtils.hasText(level)){
								compilationLevel = level;
							}
							logger.info("env CompilationLevel=" + level);
						}
						//specific compilation level for current template
						if(params.get("CompilationLevel") != null){
							String level = ((freemarker.template.SimpleScalar) params.get("CompilationLevel")).getAsString();
							if(StringUtils.hasText(level)){
								compilationLevel = level;
							}
							logger.info("tpl CompilationLevel=" + level);
						}
						CompilerOptions options = new CompilerOptions();
						options.setRemoveUnusedLocalVars(true);
						options.setRemoveDeadCode(true);
						options.setRemoveUnusedClassProperties(true);
						options.setRemoveUnusedVars(true);
						options.setRemoveAbstractMethods(true);
						options.setRemoveClosureAsserts(true);
						
						if(env.getVariable("PrettyPrint") != null){
							String prettyPrint = ((freemarker.template.SimpleScalar)env.getVariable("PrettyPrint")).getAsString();
							options.setPrettyPrint("true".equalsIgnoreCase(prettyPrint));
							logger.info("env PrettyPrint=" + prettyPrint);
						}else{						
							options.setPrettyPrint(false);
						}
						if("WHITESPACE_ONLY".equalsIgnoreCase(compilationLevel)){
							CompilationLevel.WHITESPACE_ONLY.setOptionsForCompilationLevel(options);
						}
						else if("SIMPLE_OPTIMIZATIONS".equalsIgnoreCase(compilationLevel)){
							CompilationLevel.SIMPLE_OPTIMIZATIONS.setOptionsForCompilationLevel(options);							
						}else{
							CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
						}
						//WarningLevel.QUIET.setOptionsForWarningLevel(options);
						WarningLevel.DEFAULT.setOptionsForWarningLevel(options);
						
						String externs = "";
						if(env.getVariable("ShrinksafeExterns") != null){
							externs = ((freemarker.template.SimpleScalar)env.getVariable("ShrinksafeExterns")).getAsString();
						}
						SourceFile externSource = SourceFile.fromCode("externs.js",externs);						
						SourceFile origin = SourceFile.fromCode("origin.js",script);
						
						Compiler compiler = new Compiler();
						compiler.compile(externSource, origin, options);
						logger.info("source compile completed...CompilationLevel=" + compilationLevel);
						
						JSError[] warnings = compiler.getWarnings();
						JSError[] errors = compiler.getErrors();
						if(warnings.length > 0 || errors.length > 0){
							for (JSError e : warnings) {							
								logger.error("Waring:[{}] line number:[{}] code:[{}]",e.description, e.lineNumber,origin.getLine(e.lineNumber));
							}
							for (JSError e : errors) {
								logger.error("Error:[{}] line number:[{}] code:[{}]",e.description, e.lineNumber,origin.getLine(e.lineNumber));
							} 
							out.write(origin.getCode());
							//logger.error("Original code:{}",origin.getCode());
						}
						else{
							String source = compiler.toSource();							
							out.write(source);
							if (cached && key != null) {
								cache.put(key, source);
							}
							logger.info("Write source completed");		
						}	
					}catch(TemplateException e){
						logger.error("Error occurred {}",e);
					}									
				}			
		}		
	}
}
